import isEditable from 'dom-element-is-natively-editable';

const CTRL = 1 << 0;
const META = 1 << 1;
const ALT = 1 << 2;
const SHIFT = 1 << 3;

// Key Events
export const KeyEvents = {
    DOWN: 1 << 0,
    PRESS: 1 << 1,
    UP: 1 << 2,
    INPUT: 1 << 3,
};
KeyEvents.ALL = KeyEvents.DOWN | KeyEvents.PRESS | KeyEvents.UP | KeyEvents.INPUT;

/**
 * Represents a keystroke, or a single key code with a set of active modifiers.
 *
 * @class Keystroke
 */
export class Keystroke {
    /**
     * @param {number} modifiers A bitmask formed by CTRL, META, ALT, and SHIFT.
     * @param {number} keyCode
     */
    constructor(modifiers, keyCode) {
        this.modifiers = modifiers;
        this.ctrlKey = !!(modifiers & CTRL);
        this.metaKey = !!(modifiers & META);
        this.altKey = !!(modifiers & ALT);
        this.shiftKey = !!(modifiers & SHIFT);
        this.keyCode = keyCode;
    }

    /**
     * Gets the bitmask value for the "control" modifier.
     *
     * @type {number}
     */
    static CTRL = CTRL;

    /**
     * Gets the bitmask value for the "meta" modifier.
     *
     * @return {number}
     */
    static META = META;

    /**
     * Gets the bitmask value for the "alt" modifier.
     *
     * @return {number}
     */
    static ALT = ALT;

    /**
     * Gets the bitmask value for the "shift" modifier.
     *
     * @return {number}
     */
    static SHIFT = SHIFT;
}

/**
 * Simulates a keyboard with a particular key-to-character and key-to-action
 * mapping. Use `US_ENGLISH` to get a pre-configured keyboard.
 */
export class Keyboard {
    /**
     * @param {Object.<number, Keystroke>} charCodeKeyCodeMap
     * @param {Object.<string, number>} actionKeyCodeMap
     */
    constructor(charCodeKeyCodeMap, actionKeyCodeMap) {
        this._charCodeKeyCodeMap = charCodeKeyCodeMap;
        this._actionKeyCodeMap = actionKeyCodeMap;
    }

    /**
     * Determines the character code generated by pressing the given keystroke.
     *
     * @param {Keystroke} keystroke
     * @return {?number}
     */
    charCodeForKeystroke(keystroke) {
        let map = this._charCodeKeyCodeMap;
        for (let charCode in map) {
            if (Object.prototype.hasOwnProperty.call(map, charCode)) {
                let keystrokeForCharCode = map[charCode];
                if (keystroke.keyCode === keystrokeForCharCode.keyCode && keystroke.modifiers === keystrokeForCharCode.modifiers) {
                    return parseInt(charCode, 10);
                }
            }
        }
        return null;
    }

    /**
     * Creates an event ready for dispatching onto the given target.
     *
     * @param {string} type One of "keydown", "keypress", "keyup", "textInput" or "input".
     * @param {Keystroke} keystroke
     * @param {HTMLElement} target
     * @return {Event}
     */
    createEventFromKeystroke(type, keystroke, target) {
        const document = target.ownerDocument;
        const window = document.defaultView;
        const Event = window.Event;

        let event;

        try {
            event = new Event(type);
        } catch (e) {
            event = document.createEvent('UIEvents');
        }

        event.initEvent(type, true, true);

        switch (type) {
            case 'textInput':
                event.data = String.fromCharCode(this.charCodeForKeystroke(keystroke));
                break;

            case 'keydown':
            case 'keypress':
            case 'keyup':
                event.shiftKey = keystroke.shiftKey;
                event.altKey = keystroke.altKey;
                event.metaKey = keystroke.metaKey;
                event.ctrlKey = keystroke.ctrlKey;
                event.keyCode = type === 'keypress' ? this.charCodeForKeystroke(keystroke) : keystroke.keyCode;
                event.charCode = type === 'keypress' ? event.keyCode : 0;
                event.which = event.keyCode;
                break;
        }

        return event;
    }

    /**
     * Fires the correct sequence of events on the given target as if the given
     * action was undertaken by a human.
     *
     * @param {string} action e.g. "alt+shift+left" or "backspace"
     * @param {HTMLElement} target
     */
    dispatchEventsForAction(action, target) {
        const keystroke = this.keystrokeForAction(action);
        this.dispatchEventsForKeystroke(keystroke, target);
    }

    /**
     * Fires the correct sequence of events on the given target as if the given
     * input had been typed by a human.
     *
     * @param {string} input
     * @param {HTMLElement} target
     */
    dispatchEventsForInput(input, target) {
        let currentModifierState = 0;
        for (let i = 0, length = input.length; i < length; i++) {
            const keystroke = this.keystrokeForCharCode(input.charCodeAt(i));
            if (!keystroke) continue;

            this.dispatchModifierStateTransition(target, currentModifierState, keystroke.modifiers);
            this.dispatchEventsForKeystroke(keystroke, target, false);
            currentModifierState = keystroke.modifiers;
        }
        this.dispatchModifierStateTransition(target, currentModifierState, 0);
    }

    /**
     * Fires the correct sequence of events on the given target as if the given
     * keystroke was performed by a human. When simulating, for example, typing
     * the letter "A" (assuming a U.S. English keyboard) then the sequence will
     * look like this:
     *
     *   keydown   keyCode=16 (SHIFT) charCode=0      shiftKey=true
     *   keydown   keyCode=65 (A)     charCode=0      shiftKey=true
     *   keypress  keyCode=65 (A)     charCode=65 (A) shiftKey=true
     *   textInput data=A
     *   input
     *   keyup     keyCode=65 (A)     charCode=0      shiftKey=true
     *   keyup     keyCode=16 (SHIFT) charCode=0      shiftKey=false
     *
     * If the keystroke would not cause a character to be input, such as when
     * pressing alt+shift+left, the sequence looks like this:
     *
     *   keydown   keyCode=16 (SHIFT) charCode=0 altKey=false shiftKey=true
     *   keydown   keyCode=18 (ALT)   charCode=0 altKey=true  shiftKey=true
     *   keydown   keyCode=37 (LEFT)  charCode=0 altKey=true  shiftKey=true
     *   keyup     keyCode=37 (LEFT)  charCode=0 altKey=true  shiftKey=true
     *   keyup     keyCode=18 (ALT)   charCode=0 altKey=false shiftKey=true
     *   keyup     keyCode=16 (SHIFT) charCode=0 altKey=false shiftKey=false
     *
     * To disable handling of modifier keys, call with `transitionModifers` set
     * to false. Doing so will omit the keydown and keyup events associated with
     * shift, ctrl, alt, and meta keys surrounding the actual keystroke.
     *
     * @param {Keystroke} keystroke
     * @param {HTMLElement} target
     * @param {boolean=} transitionModifiers
     * @param {number} events
     */
    dispatchEventsForKeystroke(keystroke, target, transitionModifiers = true, events = KeyEvents.ALL) {
        if (!keystroke) return;

        if (transitionModifiers) {
            this.dispatchModifierStateTransition(target, 0, keystroke.modifiers, events);
        }

        let keydownEvent;
        if (events & KeyEvents.DOWN) {
            keydownEvent = this.createEventFromKeystroke('keydown', keystroke, target);
        }

        if (keydownEvent && target.dispatchEvent(keydownEvent) && this.targetCanReceiveTextInput(target)) {
            let keypressEvent;
            if (events & KeyEvents.PRESS) {
                keypressEvent = this.createEventFromKeystroke('keypress', keystroke, target);
            }
            if (keypressEvent && keypressEvent.charCode && target.dispatchEvent(keypressEvent)) {
                if (events & KeyEvents.INPUT) {
                    const textinputEvent = this.createEventFromKeystroke('textInput', keystroke, target);
                    target.dispatchEvent(textinputEvent);

                    const inputEvent = this.createEventFromKeystroke('input', keystroke, target);
                    target.dispatchEvent(inputEvent);
                }
            }
        }

        if (events & KeyEvents.UP) {
            const keyupEvent = this.createEventFromKeystroke('keyup', keystroke, target);
            target.dispatchEvent(keyupEvent);
        }

        if (transitionModifiers) {
            this.dispatchModifierStateTransition(target, keystroke.modifiers, 0);
        }
    }

    /**
     * Transitions from one modifier state to another by dispatching key events.
     *
     * @param {EventTarget} target
     * @param {number} fromModifierState
     * @param {number} toModifierState
     * @param {number} events
     * @private
     */
    dispatchModifierStateTransition(target, fromModifierState, toModifierState, events = KeyEvents.ALL) {
        let currentModifierState = fromModifierState;
        let didHaveMeta = (fromModifierState & META) === META;
        let willHaveMeta = (toModifierState & META) === META;
        let didHaveCtrl = (fromModifierState & CTRL) === CTRL;
        let willHaveCtrl = (toModifierState & CTRL) === CTRL;
        let didHaveShift = (fromModifierState & SHIFT) === SHIFT;
        let willHaveShift = (toModifierState & SHIFT) === SHIFT;
        let didHaveAlt = (fromModifierState & ALT) === ALT;
        let willHaveAlt = (toModifierState & ALT) === ALT;

        const includeKeyUp = events & KeyEvents.UP;
        const includeKeyDown = events & KeyEvents.DOWN;

        if (includeKeyUp && didHaveMeta === true && willHaveMeta === false) {
            // Release the meta key.
            currentModifierState &= ~META;
            target.dispatchEvent(this.createEventFromKeystroke('keyup', new Keystroke(currentModifierState, this._actionKeyCodeMap.META), target));
        }

        if (includeKeyUp && didHaveCtrl === true && willHaveCtrl === false) {
            // Release the ctrl key.
            currentModifierState &= ~CTRL;
            target.dispatchEvent(this.createEventFromKeystroke('keyup', new Keystroke(currentModifierState, this._actionKeyCodeMap.CTRL), target));
        }

        if (includeKeyUp && didHaveShift === true && willHaveShift === false) {
            // Release the shift key.
            currentModifierState &= ~SHIFT;
            target.dispatchEvent(this.createEventFromKeystroke('keyup', new Keystroke(currentModifierState, this._actionKeyCodeMap.SHIFT), target));
        }

        if (includeKeyUp && didHaveAlt === true && willHaveAlt === false) {
            // Release the alt key.
            currentModifierState &= ~ALT;
            target.dispatchEvent(this.createEventFromKeystroke('keyup', new Keystroke(currentModifierState, this._actionKeyCodeMap.ALT), target));
        }

        if (includeKeyDown && didHaveMeta === false && willHaveMeta === true) {
            // Press the meta key.
            currentModifierState |= META;
            target.dispatchEvent(this.createEventFromKeystroke('keydown', new Keystroke(currentModifierState, this._actionKeyCodeMap.META), target));
        }

        if (includeKeyDown && didHaveCtrl === false && willHaveCtrl === true) {
            // Press the ctrl key.
            currentModifierState |= CTRL;
            target.dispatchEvent(this.createEventFromKeystroke('keydown', new Keystroke(currentModifierState, this._actionKeyCodeMap.CTRL), target));
        }

        if (includeKeyDown && didHaveShift === false && willHaveShift === true) {
            // Press the shift key.
            currentModifierState |= SHIFT;
            target.dispatchEvent(this.createEventFromKeystroke('keydown', new Keystroke(currentModifierState, this._actionKeyCodeMap.SHIFT), target));
        }

        if (includeKeyDown && didHaveAlt === false && willHaveAlt === true) {
            // Press the alt key.
            currentModifierState |= ALT;
            target.dispatchEvent(this.createEventFromKeystroke('keydown', new Keystroke(currentModifierState, this._actionKeyCodeMap.ALT), target));
        }

        if (currentModifierState !== toModifierState) {
            throw new Error(`internal error, expected modifier state: ${toModifierState}` + `, got: ${currentModifierState}`);
        }
    }

    /**
     * Returns the keystroke associated with the given action.
     *
     * @param {string} action
     * @return {?Keystroke}
     */
    keystrokeForAction(action) {
        let keyCode = null;
        let modifiers = 0;

        // Note: when it comes to a single character as '+',
        // should not take it as a key combiniation (no action.split)
        let parts = action.length === 1 ? [action] : action.split('+');
        let lastPart = parts.pop();

        parts.forEach((part) => {
            switch (part.toUpperCase()) {
                case 'CTRL':
                    modifiers |= CTRL;
                    break;
                case 'META':
                    modifiers |= META;
                    break;
                case 'ALT':
                    modifiers |= ALT;
                    break;
                case 'SHIFT':
                    modifiers |= SHIFT;
                    break;
                default:
                    console.error('parts', parts);
                    throw new Error(`in "${action}", invalid modifier: ${part}`);
            }
        });

        if (lastPart.toUpperCase() in this._actionKeyCodeMap) {
            keyCode = this._actionKeyCodeMap[lastPart.toUpperCase()];
        } else if (lastPart.length === 1) {
            let lastPartKeystroke = this.keystrokeForCharCode(lastPart.charCodeAt(0));
            if (!lastPartKeystroke) return null;

            modifiers |= lastPartKeystroke.modifiers;
            keyCode = lastPartKeystroke.keyCode;
        } else {
            throw new Error(`in "${action}", invalid action: ${lastPart}`);
        }

        return new Keystroke(modifiers, keyCode);
    }

    /**
     * Gets the keystroke used to generate the given character code.
     *
     * @param {number} charCode
     * @return {?Keystroke}
     */
    keystrokeForCharCode(charCode) {
        return this._charCodeKeyCodeMap[charCode] || null;
    }

    /**
     * @param {EventTarget} target
     * @private
     */
    targetCanReceiveTextInput(target) {
        if (!target) {
            return false;
        }

        return isEditable(target);
    }
}

const US_ENGLISH_CHARCODE_KEYCODE_MAP = {
    32: new Keystroke(0, 32), // <space>
    33: new Keystroke(SHIFT, 49), // !
    34: new Keystroke(SHIFT, 222), // "
    35: new Keystroke(SHIFT, 51), // #
    36: new Keystroke(SHIFT, 52), // $
    37: new Keystroke(SHIFT, 53), // %
    38: new Keystroke(SHIFT, 55), // &
    39: new Keystroke(0, 222), // '
    40: new Keystroke(SHIFT, 57), // (
    41: new Keystroke(SHIFT, 48), // )
    42: new Keystroke(SHIFT, 56), // *
    43: new Keystroke(SHIFT, 187), // +
    44: new Keystroke(0, 188), // ,
    45: new Keystroke(0, 189), // -
    46: new Keystroke(0, 190), // .
    47: new Keystroke(0, 191), // /
    48: new Keystroke(0, 48), // 0
    49: new Keystroke(0, 49), // 1
    50: new Keystroke(0, 50), // 2
    51: new Keystroke(0, 51), // 3
    52: new Keystroke(0, 52), // 4
    53: new Keystroke(0, 53), // 5
    54: new Keystroke(0, 54), // 6
    55: new Keystroke(0, 55), // 7
    56: new Keystroke(0, 56), // 8
    57: new Keystroke(0, 57), // 9
    58: new Keystroke(SHIFT, 186), // :
    59: new Keystroke(0, 186), // ;
    60: new Keystroke(SHIFT, 188), // <
    61: new Keystroke(0, 187), // =
    62: new Keystroke(SHIFT, 190), // >
    63: new Keystroke(SHIFT, 191), // ?
    64: new Keystroke(SHIFT, 50), // @
    65: new Keystroke(SHIFT, 65), // A
    66: new Keystroke(SHIFT, 66), // B
    67: new Keystroke(SHIFT, 67), // C
    68: new Keystroke(SHIFT, 68), // D
    69: new Keystroke(SHIFT, 69), // E
    70: new Keystroke(SHIFT, 70), // F
    71: new Keystroke(SHIFT, 71), // G
    72: new Keystroke(SHIFT, 72), // H
    73: new Keystroke(SHIFT, 73), // I
    74: new Keystroke(SHIFT, 74), // J
    75: new Keystroke(SHIFT, 75), // K
    76: new Keystroke(SHIFT, 76), // L
    77: new Keystroke(SHIFT, 77), // M
    78: new Keystroke(SHIFT, 78), // N
    79: new Keystroke(SHIFT, 79), // O
    80: new Keystroke(SHIFT, 80), // P
    81: new Keystroke(SHIFT, 81), // Q
    82: new Keystroke(SHIFT, 82), // R
    83: new Keystroke(SHIFT, 83), // S
    84: new Keystroke(SHIFT, 84), // T
    85: new Keystroke(SHIFT, 85), // U
    86: new Keystroke(SHIFT, 86), // V
    87: new Keystroke(SHIFT, 87), // W
    88: new Keystroke(SHIFT, 88), // X
    89: new Keystroke(SHIFT, 89), // Y
    90: new Keystroke(SHIFT, 90), // Z
    91: new Keystroke(0, 219), // [
    92: new Keystroke(0, 220), // \
    93: new Keystroke(0, 221), // ]
    94: new Keystroke(SHIFT, 54), // ^
    95: new Keystroke(SHIFT, 189), // _
    96: new Keystroke(0, 192), // `
    97: new Keystroke(0, 65), // a
    98: new Keystroke(0, 66), // b
    99: new Keystroke(0, 67), // c
    100: new Keystroke(0, 68), // d
    101: new Keystroke(0, 69), // e
    102: new Keystroke(0, 70), // f
    103: new Keystroke(0, 71), // g
    104: new Keystroke(0, 72), // h
    105: new Keystroke(0, 73), // i
    106: new Keystroke(0, 74), // j
    107: new Keystroke(0, 75), // k
    108: new Keystroke(0, 76), // l
    109: new Keystroke(0, 77), // m
    110: new Keystroke(0, 78), // n
    111: new Keystroke(0, 79), // o
    112: new Keystroke(0, 80), // p
    113: new Keystroke(0, 81), // q
    114: new Keystroke(0, 82), // r
    115: new Keystroke(0, 83), // s
    116: new Keystroke(0, 84), // t
    117: new Keystroke(0, 85), // u
    118: new Keystroke(0, 86), // v
    119: new Keystroke(0, 87), // w
    120: new Keystroke(0, 88), // x
    121: new Keystroke(0, 89), // y
    122: new Keystroke(0, 90), // z
    123: new Keystroke(SHIFT, 219), // {
    124: new Keystroke(SHIFT, 220), // |
    125: new Keystroke(SHIFT, 221), // }
    126: new Keystroke(SHIFT, 192), // ~
};

const US_ENGLISH_ACTION_KEYCODE_MAP = {
    BACKSPACE: 8,
    TAB: 9,
    ENTER: 13,
    SHIFT: 16,
    CTRL: 17,
    ALT: 18,
    PAUSE: 19,
    CAPSLOCK: 20,
    ESCAPE: 27,
    PAGEUP: 33,
    PAGEDOWN: 34,
    END: 35,
    HOME: 36,
    LEFT: 37,
    UP: 38,
    RIGHT: 39,
    DOWN: 40,
    INSERT: 45,
    DELETE: 46,
    META: 91,
    F1: 112,
    F2: 113,
    F3: 114,
    F4: 115,
    F5: 116,
    F6: 117,
    F7: 118,
    F8: 119,
    F9: 120,
    F10: 121,
    F11: 122,
    F12: 123,
};

/**
 * Gets a keyboard instance configured as a U.S. English keyboard would be.
 *
 * @return {Keyboard}
 */
Keyboard.US_ENGLISH = new Keyboard(US_ENGLISH_CHARCODE_KEYCODE_MAP, US_ENGLISH_ACTION_KEYCODE_MAP);
